// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Tracing;
using System.Linq.Expressions;
using System.Text;
using Microsoft.Extensions.DependencyInjection.ServiceLookup;

namespace Microsoft.Extensions.DependencyInjection
{
#pragma warning disable ESGEN001 // EventSource class is not partial. It's blocked by https://github.com/dotnet/runtime/issues/121205
    [EventSource(Name = "Microsoft-Extensions-DependencyInjection")]
    internal sealed class DependencyInjectionEventSource : EventSource
#pragma warning restore ESGEN001
    {
        public static readonly DependencyInjectionEventSource Log = new DependencyInjectionEventSource();

        public static class Keywords
        {
            public const EventKeywords ServiceProviderInitialized = (EventKeywords)0x1;
        }

        // Event source doesn't support large payloads so we chunk large payloads like formatted call site tree and descriptors
        private const int MaxChunkSize = 10 * 1024;

        private readonly List<WeakReference<ServiceProvider>> _providers = new();

        // This event source uses EtwSelfDescribingEventFormat for backward compatibility.
        private DependencyInjectionEventSource() : base(EventSourceSettings.EtwSelfDescribingEventFormat)
        {
        }

        // There is a risk that each ServiceProviderBuilt call only finds one entry to remove, and the next call will clean the list again and spend O(n) time on that.
        // So instead of tying the cleaning to the resizing, it might be better to have a separate counter.
        // For example: the current capacity is 1024, but the last time the list was cleaned, 1000 valid entries were left, so now we'll let the list grow to 2000 entries before cleaning it again.
        private int? _survivingProvidersCount;

        // NOTE
        // - The 'Start' and 'Stop' suffixes on the following event names have special meaning in EventSource. They
        //   enable creating 'activities'.
        //   For more information, take a look at the following blog post:
        //   https://blogs.msdn.microsoft.com/vancem/2015/09/14/exploring-eventsource-activity-correlation-and-causation-features/
        // - A stop event's event id must be next one after its start event.
        // - Avoid renaming methods or parameters marked with EventAttribute. EventSource uses these to form the event object.

        [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode",
            Justification = "Parameters to this method are primitive and are trimmer safe.")]
        [Event(1, Level = EventLevel.Verbose)]
        private void CallSiteBuilt(string serviceType, string callSite, int chunkIndex, int chunkCount, int serviceProviderHashCode)
        {
            WriteEvent(1, serviceType, callSite, chunkIndex, chunkCount, serviceProviderHashCode);
        }

        [Event(2, Level = EventLevel.Verbose)]
        public void ServiceResolved(string serviceType, int serviceProviderHashCode)
        {
            WriteEvent(2, serviceType, serviceProviderHashCode);
        }

        [Event(3, Level = EventLevel.Verbose)]
        public void ExpressionTreeGenerated(string serviceType, int nodeCount, int serviceProviderHashCode)
        {
            WriteEvent(3, serviceType, nodeCount, serviceProviderHashCode);
        }

        [Event(4, Level = EventLevel.Verbose)]
        public void DynamicMethodBuilt(string serviceType, int methodSize, int serviceProviderHashCode)
        {
            WriteEvent(4, serviceType, methodSize, serviceProviderHashCode);
        }

        [Event(5, Level = EventLevel.Verbose)]
        public void ScopeDisposed(int serviceProviderHashCode, int scopedServicesResolved, int disposableServices)
        {
            WriteEvent(5, serviceProviderHashCode, scopedServicesResolved, disposableServices);
        }

        [Event(6, Level = EventLevel.Error)]
        public void ServiceRealizationFailed(string? exceptionMessage, int serviceProviderHashCode)
        {
            WriteEvent(6, exceptionMessage, serviceProviderHashCode);
        }

        [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode",
            Justification = "Parameters to this method are primitive and are trimmer safe.")]
        [Event(7, Level = EventLevel.Informational, Keywords = Keywords.ServiceProviderInitialized)]
        private void ServiceProviderBuilt(int serviceProviderHashCode, int singletonServices, int scopedServices, int transientServices, int closedGenericsServices, int openGenericsServices)
        {
            WriteEvent(7, serviceProviderHashCode, singletonServices, scopedServices, transientServices, closedGenericsServices, openGenericsServices);
        }

        [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode",
            Justification = "Parameters to this method are primitive and are trimmer safe.")]
        [Event(8, Level = EventLevel.Informational, Keywords = Keywords.ServiceProviderInitialized)]
        private void ServiceProviderDescriptors(int serviceProviderHashCode, string descriptors, int chunkIndex, int chunkCount)
        {
            WriteEvent(8, serviceProviderHashCode, descriptors, chunkIndex, chunkCount);
        }

        [NonEvent]
        public void ServiceResolved(ServiceProvider provider, Type serviceType)
        {
            if (IsEnabled(EventLevel.Verbose, EventKeywords.All))
            {
                ServiceResolved(serviceType.ToString(), provider.GetHashCode());
            }
        }

        [NonEvent]
        public void CallSiteBuilt(ServiceProvider provider, Type serviceType, ServiceCallSite callSite)
        {
            if (IsEnabled(EventLevel.Verbose, EventKeywords.All))
            {
                string format = CallSiteJsonFormatter.Instance.Format(callSite);
                int chunkCount = format.Length / MaxChunkSize + (format.Length % MaxChunkSize > 0 ? 1 : 0);

                int providerHashCode = provider.GetHashCode();
                for (int i = 0; i < chunkCount; i++)
                {
                    CallSiteBuilt(
                        serviceType.ToString(),
                        format.Substring(i * MaxChunkSize, Math.Min(MaxChunkSize, format.Length - i * MaxChunkSize)), i, chunkCount,
                        providerHashCode);
                }
            }
        }

        [NonEvent]
        public void DynamicMethodBuilt(ServiceProvider provider, Type serviceType, int methodSize)
        {
            if (IsEnabled(EventLevel.Verbose, EventKeywords.All))
            {
                DynamicMethodBuilt(serviceType.ToString(), methodSize, provider.GetHashCode());
            }
        }

        [NonEvent]
        public void ServiceRealizationFailed(Exception exception, int serviceProviderHashCode)
        {
            if (IsEnabled(EventLevel.Error, EventKeywords.All))
            {
                ServiceRealizationFailed(exception.ToString(), serviceProviderHashCode);
            }
        }

        [NonEvent]
        public void ServiceProviderBuilt(ServiceProvider provider)
        {
            lock (_providers)
            {
                int providersCount = _providers.Count;
                if (providersCount > 0 &&
                    (_survivingProvidersCount is int spc ? (uint)providersCount >= 2 * (uint)spc : providersCount == _providers.Capacity))
                {
                    _providers.RemoveAll(static p => !p.TryGetTarget(out _));
                    _survivingProvidersCount = _providers.Count;
                }

                _providers.Add(new WeakReference<ServiceProvider>(provider));
            }

            WriteServiceProviderBuilt(provider);
        }

        [NonEvent]
        public void ServiceProviderDisposed(ServiceProvider provider)
        {
            lock (_providers)
            {
                for (int i = _providers.Count - 1; i >= 0; i--)
                {
                    // remove the provider, along with any stale references
                    WeakReference<ServiceProvider> reference = _providers[i];
                    if (!reference.TryGetTarget(out ServiceProvider? target) || target == provider)
                    {
                        _providers.RemoveAt(i);

                        if (target is not null)
                        {
                            break;
                        }
                    }
                }
            }
        }

        [NonEvent]
        private void WriteServiceProviderBuilt(ServiceProvider provider)
        {
            if (IsEnabled(EventLevel.Informational, Keywords.ServiceProviderInitialized))
            {
                int singletonServices = 0;
                int scopedServices = 0;
                int transientServices = 0;
                int closedGenericsServices = 0;
                int openGenericsServices = 0;

                StringBuilder descriptorBuilder = new StringBuilder("{ \"descriptors\":[ ");
                bool firstDescriptor = true;
                foreach (ServiceDescriptor descriptor in provider.CallSiteFactory.Descriptors)
                {
                    if (firstDescriptor)
                    {
                        firstDescriptor = false;
                    }
                    else
                    {
                        descriptorBuilder.Append(", ");
                    }

                    AppendServiceDescriptor(descriptorBuilder, descriptor);

                    switch (descriptor.Lifetime)
                    {
                        case ServiceLifetime.Singleton:
                            singletonServices++;
                            break;
                        case ServiceLifetime.Scoped:
                            scopedServices++;
                            break;
                        case ServiceLifetime.Transient:
                            transientServices++;
                            break;
                    }

                    if (descriptor.ServiceType.IsGenericType)
                    {
                        if (descriptor.ServiceType.IsConstructedGenericType)
                        {
                            closedGenericsServices++;
                        }
                        else
                        {
                            openGenericsServices++;
                        }
                    }
                }
                descriptorBuilder.Append(" ] }");

                int providerHashCode = provider.GetHashCode();
                ServiceProviderBuilt(providerHashCode, singletonServices, scopedServices, transientServices, closedGenericsServices, openGenericsServices);

                string descriptorString = descriptorBuilder.ToString();
                int chunkCount = descriptorString.Length / MaxChunkSize + (descriptorString.Length % MaxChunkSize > 0 ? 1 : 0);

                for (int i = 0; i < chunkCount; i++)
                {
                    ServiceProviderDescriptors(
                        providerHashCode,
                        descriptorString.Substring(i * MaxChunkSize, Math.Min(MaxChunkSize, descriptorString.Length - i * MaxChunkSize)), i, chunkCount);
                }
            }
        }

        [NonEvent]
        private static void AppendServiceDescriptor(StringBuilder builder, ServiceDescriptor descriptor)
        {
            builder.Append("{ \"serviceType\": \"");
            builder.Append(descriptor.ServiceType);
            builder.Append("\", \"lifetime\": \"");
            builder.Append(descriptor.Lifetime);
            builder.Append("\", ");

            if (descriptor.HasImplementationType())
            {
                builder.Append("\"implementationType\": \"");
                builder.Append(descriptor.GetImplementationType());
            }
            else if (!descriptor.IsKeyedService && descriptor.ImplementationFactory != null)
            {
                builder.Append("\"implementationFactory\": \"");
                builder.Append(descriptor.ImplementationFactory.Method);
            }
            else if (descriptor.IsKeyedService && descriptor.KeyedImplementationFactory != null)
            {
                builder.Append("\"implementationFactory\": \"");
                builder.Append(descriptor.KeyedImplementationFactory.Method);
            }
            else if (descriptor.HasImplementationInstance())
            {
                object? instance = descriptor.GetImplementationInstance();
                Debug.Assert(instance != null, "descriptor.ImplementationInstance != null");
                builder.Append("\"implementationInstance\": \"");
                builder.Append(instance.GetType());
                builder.Append(" (instance)");
            }
            else
            {
                builder.Append("\"unknown\": \"");
            }

            builder.Append("\" }");
        }

        protected override void OnEventCommand(EventCommandEventArgs command)
        {
            if (command.Command == EventCommand.Enable)
            {
                // When this EventSource becomes enabled, write out the existing ServiceProvider information
                // because building the ServiceProvider happens early in the process. This way a listener
                // can get this information, even if they attach while the process is running.

                lock (_providers)
                {
                    foreach (WeakReference<ServiceProvider> reference in _providers)
                    {
                        if (reference.TryGetTarget(out ServiceProvider? provider))
                        {
                            WriteServiceProviderBuilt(provider);
                        }
                    }
                }
            }
        }
    }

    internal static class DependencyInjectionEventSourceExtensions
    {
        // This is an extension method because this assembly is trimmed at a "type granular" level in Blazor,
        // and the whole DependencyInjectionEventSource type can't be trimmed. So extracting this to a separate
        // type allows for the System.Linq.Expressions usage to be trimmed by the ILLinker.
        public static void ExpressionTreeGenerated(this DependencyInjectionEventSource source, ServiceProvider provider, Type serviceType, Expression expression)
        {
            if (source.IsEnabled(EventLevel.Verbose, EventKeywords.All))
            {
                var visitor = new NodeCountingVisitor();
                visitor.Visit(expression);
                source.ExpressionTreeGenerated(serviceType.ToString(), visitor.NodeCount, provider.GetHashCode());
            }
        }

        private sealed class NodeCountingVisitor : ExpressionVisitor
        {
            public int NodeCount { get; private set; }

            [return: NotNullIfNotNull(nameof(e))]
            public override Expression? Visit(Expression? e)
            {
                base.Visit(e);
                NodeCount++;
                return e;
            }
        }
    }
}
